﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using OpenTK;
using OpenTK.Input;
using System.Runtime.InteropServices;
using System.IO;
using System.Windows.Forms;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoStrategy.RenderSystem
{
    public delegate long DPrecisionTimerMillis();

    public partial class TerrainRenderer 
    {
        private Stack<Point>[] m_BuildingPipeline;
        private bool m_IsSelectionPass = false;
        private Matrix4 m_ProjMatrix, m_ViewMatrix, m_ModelMatrix;
        private GLProgram m_2DSceneProgram, m_2DSceneSelectProgram, m_2DSpriteProgram;
        private TerrainMesh m_Mesh;
        private GLProgram m_Shader, m_WaterShader, m_SelectionShader, m_OcclusionShader;
        private NativeTexture[] m_GroundTextures = new NativeTexture[7];
        private int m_TimeMillisID = -1, m_SelectionOffsetXID = -1, m_SelectionOffsetYID = -1;
        private NativeTexture[] m_BuildingGridTextures;
        private readonly List<RenderableVisual> m_SelectionPassResults = new List<RenderableVisual>();
        private PointDouble m_ScreenXY = new PointDouble(0, 0);
        private readonly LinkedList<WorkItem> m_WorkItems = new LinkedList<WorkItem>();

        private readonly TopologicalList<RenderableVisual> m_SceneObjects;

        internal const int SelectionGranularity = 5;

        public event DNotifyHandler<TerrainRenderer, RenderableVisual> OnMouseEnter;
        public event DNotifyHandler<TerrainRenderer, RenderableVisual> OnMouseLeave;
        public event DNotifyHandler<TerrainRenderer, Point> OnMouseGridMove;
        public event DNotifyHandler<TerrainRenderer> OnScreenMove;
        public DPrecisionTimerMillis PrecisionTimerCallback { get; set; }

        public SpriteEngine SpriteEngine { get; private set; }
        public bool IsBuildingGridVisible { get; set; }
        public Point GridXY { get; private set; }
        public RenderableVisual MouseOverVisual { get; private set; }
        public Rectangle ScreenBounds { get; private set; }
        public Double HeightScale { get; private set; }
        public GLRenderer Renderer { get; private set; }
        public TerrainDefinition Terrain { get; private set; }
        public int Size { get; private set; }
        public PointDouble ScreenXY
        {
            get { return m_ScreenXY; }
            set
            {
                //Point newPos = new Point(
                //    Math.Max(0, Math.Min(Size - 1, value.X)),
                //    Math.Max(0, Math.Min(Size - 1, value.Y)));
                PointDouble newPos = value;

                if (newPos == m_ScreenXY)
                    return;

                m_ScreenXY = newPos;
                m_ViewMatrix = Matrix4.CreateTranslation((float)-m_ScreenXY.X, (float)-m_ScreenXY.Y, 0);

                if (OnScreenMove != null)
                    OnScreenMove(this);
            }
        }

        private Bitmap LoadImageResource(String inNameWithoutExtension)
        {
            String path = Program.GetResourcePath(inNameWithoutExtension);

            if(!File.Exists(path + ".png"))
                path = path + ".jpg";
            else
                path = path + ".png";

            if(!File.Exists(path))
                throw new FileNotFoundException(path);

            return (Bitmap)Bitmap.FromFile(path);
        }

        private class WorkItem
        {
            public Procedure Task;
            public long ExpirationMillis;
        }

        public void SynchronizeTask(Procedure inTask)
        {
            SynchronizeTask(0, inTask);
        }

        public void SynchronizeTask(long DurationMillis, Procedure inTask)
        {
            if (inTask == null)
                throw new ArgumentNullException();

            var item = new WorkItem() { Task = inTask, ExpirationMillis = PrecisionTimerCallback() + DurationMillis, };

            lock (m_WorkItems)
            {
                LinkedListNode<WorkItem> list = m_WorkItems.First;

                while (list != null)
                {
                    if (list.Value.ExpirationMillis > item.ExpirationMillis)
                    {
                        // insert work item
                        m_WorkItems.AddBefore(list, item);

                        break;
                    }

                    list = list.Next;
                }

                if (list == null)
                {
                    // insert work item
                    m_WorkItems.AddLast(item);
                }
            }
        }

        public TerrainRenderer(
            GLRenderer inRenderer, 
            TerrainDefinition inTerrain)
        {
            Renderer = inRenderer;
            Terrain = inTerrain;
            Size = Terrain.Size;
            HeightScale = Terrain.Config.HeightScale;
            m_SceneObjects = new TopologicalList<RenderableVisual>(TerrainMesh.BlockSize, Terrain.Size, Terrain.Size);
            m_ProjMatrix = m_ViewMatrix = m_ModelMatrix = Matrix4.Identity;

            if (AnimationLibrary.Instance.IsReadonly)
            {
                SpriteEngine = SpriteEngine.OpenOrCreate("./Textures.cache", 2048);

                // are cached textures outdated?
                if (!SpriteEngine.CustomChecksums.Contains(AnimationLibrary.Instance.Checksum))
                {
                    if (MessageBox.Show("Texture cache either seems to be missing or outdated. To proceed (run the game), this cache must be updated which can take up to minutes, depending on your system performance. Do you want to proceed?",
                           "Generating Texture Cache...", MessageBoxButtons.YesNo) != DialogResult.Yes)
                        System.Diagnostics.Process.GetCurrentProcess().Kill();

                    SpriteEngine.BeginUpdate();
                    {
                        // add all animation libraries...
                        foreach (var animClass in AnimationLibrary.Instance.Classes)
                        {
                            string name = animClass.Name;

                            SpriteEngine.UpdateClass(animClass);
                        }

                        SpriteEngine.CustomChecksums.Add(AnimationLibrary.Instance.Checksum);
                    }
                    SpriteEngine.EndUpdate();
                }
            }

            // instantiate shaders
            String shader_TerrainVert = ParameterizeFragmentShader(File.ReadAllText(Program.GetResourcePath("Shaders/Terrain.vert")), inTerrain.Config);
            String shader_TerrainFrag = ParameterizeFragmentShader(File.ReadAllText(Program.GetResourcePath("Shaders/Terrain.frag")), inTerrain.Config);
            String shader_WaterFrag = ParameterizeFragmentShader(File.ReadAllText(Program.GetResourcePath("Shaders/Water.frag")), inTerrain.Config);
            String shader_WaterVert = ParameterizeFragmentShader(File.ReadAllText(Program.GetResourcePath("Shaders/Water.vert")), inTerrain.Config);
            String shader_SelectionVert = ParameterizeFragmentShader(File.ReadAllText(Program.GetResourcePath("Shaders/TerrainSelection.vert")), inTerrain.Config);

            m_Shader = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/Terrain.vert"), shader_TerrainVert),
                new GLShader(Program.GetResourcePath("Shaders/Terrain.frag"), shader_TerrainFrag), true);
            m_WaterShader = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/Water.vert"), shader_WaterVert),
                new GLShader(Program.GetResourcePath("Shaders/Water.frag"), shader_WaterFrag), true);
            m_SelectionShader = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/TerrainSelection.vert"), shader_SelectionVert),
                new GLShader(Program.GetResourcePath("Shaders/TerrainSelection.frag")), true);
            m_OcclusionShader = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/TerrainOcclusion.vert"), shader_SelectionVert),
                new GLShader(Program.GetResourcePath("Shaders/TerrainOcclusion.frag")), true);
            m_2DSceneSelectProgram = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/2DSceneSelect.vert")),
                new GLShader(Program.GetResourcePath("Shaders/2DSceneSelect.frag")), true);
            m_2DSceneProgram = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/2DScene.vert")),
                new GLShader(Program.GetResourcePath("Shaders/2DScene.frag")), true);
            m_2DSpriteProgram = new GLProgram(
                new GLShader(Program.GetResourcePath("Shaders/2DSprite.vert")),
                new GLShader(Program.GetResourcePath("Shaders/2DScene.frag")), true);

            if ((m_TimeMillisID = GL.GetUniformLocation(m_WaterShader.ProgramID, "timeMillis")) < 0)
                throw new ArgumentException("Terrain shader does not export uniform \"timeMillis\".");

            if ((m_SelectionOffsetXID = GL.GetUniformLocation(m_SelectionShader.ProgramID, "SelectionOffsetXID")) < 0)
                throw new ArgumentException("Terrain selection shader does not export uniform \"SelectionOffsetXID\".");

            if ((m_SelectionOffsetYID = GL.GetUniformLocation(m_SelectionShader.ProgramID, "SelectionOffsetYID")) < 0)
                throw new ArgumentException("Terrain selection shader does not export uniform \"SelectionOffsetYID\".");

            m_Mesh = new TerrainMesh(this);

            // load textures for ground plane
            m_GroundTextures[0] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Level_00"));
            m_GroundTextures[1] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Level_01"));
            m_GroundTextures[2] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Level_02"));
            m_GroundTextures[3] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Level_03"));
            m_GroundTextures[4] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Level_04"));
            m_GroundTextures[5] = m_Mesh.HeightMap;
            m_GroundTextures[6] = new NativeTexture(TextureOptions.Repeat, LoadImageResource("Terrain/Highland/High/Water"));

            // load textures for building grid
            m_BuildingGridTextures = new NativeTexture[6];
            m_BuildingPipeline = new Stack<Point>[6];

            for (int i = 0; i < m_BuildingGridTextures.Length; i++)
            {
                m_BuildingGridTextures[i] = new NativeTexture((Bitmap)Bitmap.FromFile(Program.GetResourcePath("Icons/BuildingGrid/Level_0" + i + ".png")));
                m_BuildingPipeline[i] = new Stack<Point>();
            }

            UpdateViewport();
        }

        public void UpdateViewport()
        {
            double CellWidth = 45.0 / 900 * Renderer.ViewportWidth;

            m_ProjMatrix = Matrix4.CreateOrthographicOffCenter(
                0,
                (float)CellWidth,
                (float)(CellWidth / Renderer.AspectRatio),
                0, -1000, 1000);

            m_ModelMatrix = Matrix4.CreateRotationX((float)Math.Asin(Math.PI / 4));
        }

        public void Dispose()
        {
            if (m_Shader != null)
                m_Shader.Dispose();

            if (m_WaterShader != null)
                m_WaterShader.Dispose();

            if (m_OcclusionShader != null)
                m_OcclusionShader.Dispose();

            if (m_SelectionShader != null)
                m_SelectionShader.Dispose();

            if (m_GroundTextures != null)
            {
                foreach (var tex in m_GroundTextures)
                {
                    tex.Dispose();
                }
            }

            if (m_Mesh != null)
                m_Mesh.Dispose();

            m_Mesh = null;
            m_GroundTextures = null;
            Renderer = null;
            m_Shader = null;
            m_WaterShader = null;
            m_SelectionShader = null;
            m_OcclusionShader = null;
        }

        

        
    }
}
